package com.yfs.cloud.interceptor;

import java.util.Enumeration;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.yfs.cloud.authority.UserAuthority;
import com.yfs.cloud.authority.UserAuthorityContextHolder;
import com.yfs.cloud.commons.Constants;
import com.yfs.cloud.commons.Result;
import com.yfs.cloud.util.JsonUtil;

import feign.RequestInterceptor;
import feign.RequestTemplate;

/**
* @Company: 影法師工作室
* @Description: feign请求拦截器, 网关将用户的登录id放在了请求头中传递给内部微服务, 但是当内部服务之间存在feign调用时，那么请求头信息会在feign请求的时候传递吗? 不会, 请求的头信息和请求参数都不会进行传递. 但是我们可以通过通过实现RequestInterceptor接口,完成对所有的Feign请求,传递请求头和请求参数. 注意, 这是传递原生的请求头和请求参数, 而不是feign调用的参数, feign调用的参数自己会传, 要是这个都不传的话, 那feign没人用了, 各大微服务之间的认证其实就是一个令牌的传递过程
* 这个文件放在项目的扫描目录下，则所有的feign调用都会使用此配置。如果想只有某个feign调用则可以这样设置(但配置类不能在扫描目录下)：
* @FeignClient(name = "organ",path = "/organ/OrganInfo",configuration = FeignRequestInterceptor.class)
* @author 影法師  
* @date 2020年8月26日 下午5:15:42 
* @version V1.0
 */
@SuppressWarnings("all")
@Component
public class FeignRequestInterceptor implements RequestInterceptor {

	/**
	 * Feign请求拦截器（设置请求头，传递登录信息）, 并且还传递了请求参数
	 */
	@Override
	public void apply(RequestTemplate requestTemplate) {
		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		HttpServletRequest request = attributes.getRequest();
		// 传递请求头(其实我们想传递的就是 网关处塞入的 USER_ID 头, 这样才能让微服务调用链上所有微服务都知道该用户是谁)
		Enumeration<String> headerNames = request.getHeaderNames();
		if (headerNames != null) {
			while (headerNames.hasMoreElements()) {
				String name = headerNames.nextElement();
				String values = request.getHeader(name);
				// feign 说来都是坑啊~ 这里不能传 content-length首部, 不然报 
				// org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is org.apache.catalina.connector.ClientAbortException: java.io.EOFException: Unexpected EOF read on the socket
				// 而且不能重复加入 USER_AUTHORITY_HEADER 头, 也不能重复加入  FEIGN_INNER 头
				if ("content-length".equals(name) || Constants.USER_AUTHORITY_HEADER.equals(name) || Constants.FEIGN_INNER.equals(name)) { 
	                continue;
	            }
				requestTemplate.header(name, values);
			}
		}
		// feign 请求统一塞入一个名为 INNER 的头, 这样 AuthInterceptor 就不去拦截了, 当然这里比较一刀切, 可以灵活配置哪些
		// feign 调用塞这个头, 一般这里就设置feign调用/user/findUserAuthority这个接口的时候才塞这个头, 其余 feign
		// 调用还是要认证权限的, 例如用户调用商品, 商品调用订单, 则用户 id 会一直放在头中传递下去,
		// 然后每到一个微服务处就要调用一次/user/findUserAuthority接口查看权限(所以
		// /user/findUserAuthority对应的service接口应该做缓存, 但是我图简单没做)
		// 但是feign内部调用 /user/findUserAuthority 这个接口本身(即 userFeign.findUserAuthority)
		// 就不需要再通过 AuthInterceptor鉴权了, 不然的话, 我用户访问商品服务, 然后商品服务处的AuthInterceptor自然要调用 userAuthorityFeign访问用户微服务, 则用户微服务处的 AuthInterceptor 不再拦截这个请求, 不然一般的用户不允许访问用户微服务则岂不是他连商品都看不了?
		String path = request.getRequestURI(); // 这是 /goods/findList 这种
		String url = requestTemplate.url(); // 这才是 /user/findUserAuthority?id=2020, 也就是要调用的feign接口的地址, 我们是根据这个地址来加 FEIGN_INNER头的
		// 如果是feign内部调用查询用户角色、权限信息的接口, 则放入 FEIGN_INNER 首部, 这样就不会被 AuthInterceptor 拦截了
		if (url.startsWith("/user/findUserAuthority")) {
			requestTemplate.header(Constants.FEIGN_INNER, Constants.FEIGN_INNER);
		}
		// 获取用户的角色、权限信息
		UserAuthority userAuthority = UserAuthorityContextHolder.getUserInfo();
		// 将用户角色、权限信息的json字符串塞进请求头中, 传递给下游微服务
		requestTemplate.header(Constants.USER_AUTHORITY_HEADER, JsonUtil.obj2json(Result.suc(userAuthority)));
		// 传递原请求的参数(也就是请求体), 一般项目不需要传请求体
//		Enumeration<String> bodyNames = request.getParameterNames();
//		StringBuffer body = new StringBuffer();
//		if (bodyNames != null) {
//			while (bodyNames.hasMoreElements()) {
//				String name = bodyNames.nextElement();
//				String values = request.getParameter(name);
//				body.append(name).append("=").append(values).append("&");
//			}
//		}
//		if (body.length() != 0) {
//			body.deleteCharAt(body.length() - 1);
//			requestTemplate.body(body.toString());
//		}
	}

}
